//【定义函数函数】
/**
 * 函数声明
 * 一个函数定义（也称为函数声明，或函数语句）由 function 关键字，并跟随以下部分组成：
 * 函数名称
 * 函数参数列表，包围在括号中并由逗号分隔
 * 定义函数的 JavaScript 语句，用大括号括起来，{ }
 * 举例：
 * function square(number) {
 *   return number * number;
 * }
 *
 * 参数本质上是按值传递给函数的——因此，即使函数体的代码为传递给函数的参数赋了新值，
 * 这个改变也不会反映到全局或调用该函数的代码中
 *
 * 如果你将对象作为参数传递，而函数改变了这个对象的属性，
 * 这样的改变对函数外部是可见的
 * 示例：
 * function myFunc(theObject) {
 *   theObject.make = "Toyota";
 * }
 *
 * const mycar = {
 *   make: "Honda",
 *   model: "Accord",
 *   year: 1998,
 * };
 *
 * console.log(mycar.make); // "Honda"
 * myFunc(mycar);
 * console.log(mycar.make); // "Toyota"
 *
 * 如果你将数组作为参数传递，而函数改变了这个数组的值，
 * 这样的改变对函数外部也同样可见，如下面的例子所示：
 * function myFunc(theArr) {
 *   theArr[0] = 30;
 * }
 *
 * const arr = [45];
 *
 * console.log(arr[0]); // 45
 * myFunc(arr);
 * console.log(arr[0]); // 30
 */

/**
 * 问：如果函数没由定义return语句，也就是没有显式声明返回值，那么函数返回值是什么？
 * 答：undefined 未定义
 * let x; 变量没有初始化 与 函数没有声明返回语句，结果都是 undefined
 * null 空值
 * undefined 未 定义
 */

/**
 * 函数表达式
 * 函数也可以由函数表达式创建 这样的函数可以是匿名的；它不必有一个名称
 * 示例：
 * let num = 4
 * const square = function (number) {
 *   return number * number;
 * };
 *
 * console.log(square(num)); // 16
 *
 * 然而，也可以为函数表达式提供名称，
 * 并且可以用于在函数内部代指其本身，或者在调试器堆栈跟踪中识别该函数：
 * const factorial = function fac(n) {
 *   // 三元运算符 | 三目运算符
 *   const x = 条件表达式 ? 如果成立的值或表达式 : 如果不成立的值或表达式;
 *   // 上下两种写法作用完全一样
 *   let x;
 *   if (条件表达式) {
 *       x = 如果成立的值或表达式
 *   } else {
 *       x = 如果不成立的值或表达式
 *   }
 *
 *   return n < 2 ? 1 : n * fac(n - 1);
 * };
 *
 * console.log(factorial(3)); // 6
 *
 * 在以下代码中，该函数接收由函数表达式定义的函数，
 * 并对作为第二个参数接收的数组的每个元素执行该函数：
 * function map(f, a) {
 *   const result = new Array(a.length);
 *   for (let i = 0; i < a.length; i++) {
 *     result[i] = f(a[i]);
 *   }
 *   return result;
 * }
 *
 * const cube = function (x) {
 *   return x * x * x;
 * };
 *
 * const numbers = [0, 1, 2, 5, 10];
 * console.log(map(cube, numbers)); // [0, 1, 8, 125, 1000]
 */

/**
 * 问：如果函数参数不确定时，应该如何声明函数参数列表？
 * 答：
 * 函数名 a  函数参数列表 ...args
 * function a(...args) {
 *      // args 类型是什么？数组
 *      // 怎么取参数列表的第一个参数值？
 *      args[0] // 正确写法
 *      // ...args[0] 错误写法
 *
 *      a(); // 【递归调用：函数自己调用自己】
 * }
 *
 * a();
 * new Object();
 *
 *
 * function log(a, b, c, d) {}
 * log(false, 5, {bar: 1}, 'ok');
 * 问：a, b, c, d 的值分别是什么？
 * a = false
 * b = 5
 * c = {
 *     bar: 1
 * }
 * d = 'ok'
 * 也就是说，入参的顺序，与传参的顺序完全一致
 *
 *
 * const a = {name: 'bob'}
 * function printA(a) {
 *     a.name = 'joker';
 * }
 * printA(a);
 * console.log(a);
 * // {name: 'bob'} 错
 * // {name: 'joker'} 对
 *
 * 引用类型：类、类的对象、函数、js对象，都是引用类型
 * 引用类型会将自身原本的值传入到函数中，相当于函数参数接收到了该对象的引用
 */

function myFunc(theObject) {
    theObject.make = "Toyota";
}

function myFunc1(theObject) {
    // theObject.name === ‘Toyota’
}

const mycar = new ClassName();

myFunc(mycar);

myFunc1(mycar);

//【调用函数】
/**
 * 调用函数才会以给定的参数真正执行这些动作
 * 例如一旦你定义了函数 square，你可以像这样调用它：
 * square(5);
 * 函数一定要处于调用它们的作用域中，但是函数的声明可以被提升（出现在调用语句之后）。
 * 函数声明的范围是声明它的函数（或者，如果它是在顶层声明的，则为整个程序）之内
 *
 * 函数的参数并不局限于字符串或数字。你也可以将整个对象传递给函数。
 * 函数 showProps()（其定义参见使用对象）就是一个将对象作为参数的示例
 *
 * 函数可以调用其本身。例如，下面这个函数就是用递归计算阶乘：
 * function factorial(n) {
 *   if (n === 0 || n === 1) {
 *     return 1;
 *   } else {
 *     return n * factorial(n - 1);
 *   }
 * }
 * 你可以这样计算 1 到 5 的阶乘：
 * console.log(factorial(1)); // 1
 * console.log(factorial(2)); // 2
 * console.log(factorial(3)); // 6
 * console.log(factorial(4)); // 24
 * console.log(factorial(5)); // 120
 *
 * 函数提升
 * 示例:
 * console.log(square(5)); // 25
 *
 * function square(n) {
 *   return n * n;
 * }
 * 尽管 square() 函数在声明之前被调用，但此代码的运行并没有任何错误。
 * 这是因为 JavaScript 解释器会将整个函数声明提升到当前作用域的顶部，因此上面的代码等价于：
 * // 所有函数声明实际上都位于作用域的顶部
 * function square(n) {
 *   return n * n;
 * }
 *
 * console.log(square(5)); // 25
 */

function wrapper() {
    add(a, b);

    // 虽然函数被声明在了调用的下方
    // 但是解释器会自动将函数的声明，提升到所在作用域的顶部(最上方)
    // 这就是所谓的：函数提升
    function add(a, b) {
        return a + b;
    }
}

/**
 * 问：在相同作用域下，函数的调用可以优先于函数的执行吗？
 * 答：可以
 */


//【函数作用域】
/**
 * 在函数内定义的变量不能在函数之外的任何地方访问，因为变量仅仅在该函数的作用域内定义。
 * 相对应的，一个函数可以访问定义在其范围内的任何变量和函数
 * 定义在全局域中的函数可以访问所有定义在全局域中的变量。
 * 在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量
 * 示例：
 * // 下面的变量定义在全局作用域中
 * const num1 = 20;
 * const num2 = 3;
 * const name = "Chamakh";
 *
 * // 此函数定义在全局作用域中
 * function multiply() {
 *   return num1 * num2;
 * }
 *
 * console.log(multiply()); // 60
 *
 * // 嵌套函数示例
 * function getScore() {
 *   const num1 = 2;
 *   const num2 = 3;
 *
 *   function add() {
 *     return `${name} 的得分为 ${num1 + num2}`;
 *   }
 *
 *   return add();
 * }
 *
 * console.log(getScore()); // "Chamakh 的得分为 5"
 */


//【作用域和函数栈】
/**
 * 一个函数可以指向并调用自身。有三种方法可以达到这个目的：
 * 函数名
 * arguments.callee
 * 作用域内一个指向该函数的变量名
 * 例如：
 * const foo = function bar() {
 *   // 这里编写语句
 * };
 * 在这个函数体内，以下的语句是等价的：
 * bar()
 * arguments.callee()
 * foo()
 *
 * 调用自身的函数我们称之为递归函数。在某种意义上说，递归近似于循环。
 * 两者都重复执行相同的代码，并且两者都需要一个终止条件（避免无限循环，或者在这种情况下更确切地说是无限递归）
 * 示例：
 * let x = 0;
 * // “x < 10”是循环条件
 * while (x < 10) {
 *   // 做些什么
 *   x++;
 * }
 *

 * 可以被转化成一个递归函数声明，然后调用该函数：
 * function loop(x) {
 *   // “x >= 10”是退出条件（等同于“!(x < 10)”）
 *   if (x >= 10) {
 *     return;
 *   }
 *   // 做些什么
 *   loop(x + 1); // 递归调用
 * }
 * loop(0);
 * 有些算法并不能简单的用迭代来实现。
 * 例如获取树结构（例如 DOM）中所有的节点时，使用递归实现要容易得多：
 * function walkTree(node) {
 *   if (node === null) {
 *     return;
 *   }
 *   // 对节点做些什么
 *   for (let i = 0; i < node.childNodes.length; i++) {
 *     walkTree(node.childNodes[i]);
 *   }
 * }
 * 跟 loop 函数相比，这里每个递归调用都产生了更多的递归调用
 *

 * 递归本身就使用了栈：函数栈。类似栈的行为可以在以下示例中看到：
 * function foo(i) {
 *   if (i < 0) {
 *     return;
 *   }
 *   console.log(`开始：${i}`);
 *   foo(i - 1);
 *   console.log(`结束：${i}`);
 * }
 * foo(3);
 *
 * // 打印：
 * // 开始：3
 * // 开始：2
 * // 开始：1
 * // 开始：0
 * // 结束：0
 * // 结束：1
 * // 结束：2
 * // 结束：3
 */

/**
 * 问：递归函数的含义？
 * 答：在特定情况下重复调用自身函数。
 *
 * for (let i = 2; i < 20; i += 2) {
 *     console.log(i);
 * }
 *
 * function a(i = 4) {
 *      if (i >= 20) {
 *          return
 *      }
 *
 *      console.log(i);
 *      a(i + 2);
 * }
 *
 * a(2);
 *
 * const obj = {key1: 1, key2: 2};
 * for (const key in obj) {
 *     console.log(obj[key]);
 * }
 *
 * // 1. 上面循环，循环的值是什么？
 * // 2. 对象属性名有哪些？ ['key1', 'key2']
 * // 3. 怎么得到对象中的属性名？ Object.keys(obj)：它可以将传入的对象属性名汇总成一个数组
 *
 * const keys = ['key1', 'key2']; // 【数组】
 * const keys = Object.keys(obj); // 从obj对象中汇总而来的属性名【数组】
 *
 * for (let i = 0; i < keys.length; i++) {
 *      const key = keys[i];
 *      console.log(key);
 * }
 *
 * function a (i) {
 *      if (i >= keys.length){
 *          return
 *      }
 *
 *      // 尊重现实
 *      // 你理解现实吗？
 *      console.log(obj[i]);
 *
 *      const key = keys[i];
 *      console.log(obj[key]);
 *
 *      a(i + 1);
 * }
 * a(0)
 */

/**
 * 1. 你对自己的学习状态如何评价？
 *
 * a. 感觉有时候不用汉字分不清关键字跟名学起来有点乱
 *      注释里面分不清，可以理解。
 * b. 有时候不会灵活运用 不知道怎么写 可能是缺少实例练习吧
 *      1. 看一遍（过一遍）
 *      2. 抄一遍（抄重点，多写代码）
 *      3. 讲一遍
 * c. 最近有点心浮气躁静不下来（委屈，明明写了那么多，但是还是记不住）
 * （
 *      你抄这一边的目的，并不是抱着把这些记住为核心。
 *      不要抱着我抄一遍我就会了的心态。
 *      抄不是目的，理解代码才是。
 * ）
 *
 * 2. 对于上面的学习状态，你希望得到什么样的帮助？
 *      1. 不要照抄照搬，只抄代码，并且要求结合上下文理解代码
 *      2. 这本教材内容示例非常丰富，基本涵盖了平时开发中90%以上的问题
 *      3. 理解代码，比盲目抄N遍要有效得多
 */

class A {
    static name = '123';

    constructor() {
        const obj = {key1: 1, key2: 2};
        const keys = Object.keys(obj); // ['key1', 'key2']

        for (let i = 0; i <= keys.length; i++) {
            const key = keys[i];
            // console.log(key); // 'key1' | 'key2'
            console.log(obj[key]); // obj['key1'] | obj['key2']
        }

        function self(i) {
            if (i >= keys.length) {
                return
            }

            const key = keys[i];
            // console.log(key); // 'key1' | 'key2'
            console.log(obj[key]); // obj['key1'] | obj['key2']

            self(i + 1);
        }

        self(0);

        console.log(keys);
    }

    add() {
    }
}

/**
 * 嵌套函数和闭包
 *
 * function addSquares(a, b) {
 *   function square(x) {
 *     return x * x;
 *   }
 *   return square(a) + square(b);
 * }
 *
 * console.log(addSquares(2, 3)); // 13
 * console.log(addSquares(3, 4)); // 25
 * console.log(addSquares(4, 5)); // 41
 * console.log(addSquares(4, 5)); // 41
 *
 * 由于内部函数形成了闭包，因此你可以调用外部函数并为外部函数和内部函数指定参数：
 * function outside(x) {
 *   function inside(y) {
 *     return x + y;
 *   }
 *   return inside;
 * }
 *
 * const fnInside = outside(3); // 可以这样想：给我一个可以将提供的值加上 3 的函数
 * console.log(fnInside(5)); // 8
 * console.log(outside(3)(5)); // 8
 */

/**
 * 保存变量
 * 注意到上例中 inside 被返回时 x 是怎么被保留下来的
 * 一个闭包必须保存它可见作用域中所有参数和变量
 * 因为每一次调用传入的参数都可能不同，每一次对外部函数的调用实际上重新创建了一遍这个闭包
 * 只有当返回的 inside 没有再被引用时，内存才会被释放
 */

/**
 * 多层嵌套函数
 * 函数可以被多层嵌套
 * 例如：
 * 函数（A）可以包含函数（B），后者可以再包含函数（C）
 * 这里的函数 B 和 C 都形成了闭包，所以 B 可以访问 A，C 可以访问 B
 * 此外，因为 C 可以访问 B（而 B 可以访问 A），所以 C 也可以访问 A
 * 闭包可以包含多个作用域；它们递归地包含了所有包含它的函数作用域这个称之为作用域链
 * 示例：
 * function A(x) {
 *   function B(y) {
 *     function C(z) {
 *       console.log(x + y + z);
 *     }
 *     C(3);
 *   }
 *   B(2);
 * }
 * A(1); // 打印 6（即 1 + 2 + 3）
 * 在这个示例中，C 可以访问 B 的 y 和 A 的 x
 */

/**
 * 命名冲突
 * 当同一个闭包作用域下两个参数或者变量同名时，就会产生命名冲突
 * 更近的作用域有更高的优先权，所以最近的优先级最高，最远的优先级最低
 * 这就是作用域链
 * 链的第一个元素就是最里面的作用域，最后一个元素便是最外层的作用域
 * 示例：
 * function outside() {
 *   const x = 5;
 *   function inside(x) {
 *     return x * 2;
 *   }
 *   return inside;
 * }
 *
 * console.log(outside()(10)); // 20（而不是 10）
 */


//【闭包】

/**
 * 闭包是 JavaScript 中最强大的特性之一
 * JavaScript 允许函数嵌套
 * 并且内部函数具有定义在外部函数中的所有变量和函数（以及外部函数能访问的所有变量和函数）的完全访问权限
 * 但是外部函数却不能访问定义在内部函数中的变量和函数 这给内部函数的变量提供了一种封装
 * 此外由于内部函数可以访问外部函数的作用域，
 * 因此当内部函数生存周期大于外部函数时，外部函数中定义的变量和函数的生存周期将比内部函数执行的持续时间要长
 * 当内部函数以某一种方式被任何一个外部函数之外的任何作用域访问时，就会创建闭包
 * 示例：
 * // 外部函数定义了一个名为“name”的变量
 * const pet = function (name) {
 *   const getName = function () {
 *     // 内部函数可以访问外部函数的“name”变量
 *     return name;
 *   };
 *   return getName; // 返回内部函数，从而将其暴露给外部作用域
 * };
 * const myPet = pet("Vivie");
 *
 * console.log(myPet()); // "Vivie"
 */


//【使用 arguments 对象】

/**
 * 函数的实际参数会被保存在一个类似数组的 arguments 对象中
 * 在函数内，你可以按如下方式找出传入的参数：
 * arguments[i];
 * 其中 i 是参数的序号，从 0 开始
 * 所以第一个传入函数的参数会是 arguments[0]
 * 参数的数量由 arguments.length 表示
 *
 * 使用 arguments 对象，你可以处理比声明更多的参数来调用函数
 * 这在你事先不知道会需要将多少参数传递给函数时十分有用
 * 你可以用 arguments.length 来获得实际传递给函数的参数的数量，然后用 arguments 对象来访问每个参数
 *
 * 考虑有一个用来连接字符串的函数
 * 唯一正式的参数是在连接后的字符串中用来分隔各个连接部分的字符。该函数定义如下：
 * 考虑有一个用来连接字符串的函数
 * 唯一正式的参数是在连接后的字符串中用来分隔各个连接部分的字符
 * 该函数定义如下：
 * function myConcat(separator) {
 *   let result = ""; // 初始化列表
 *   // 迭代 arguments
 *   for (let i = 1; i < arguments.length; i++) {
 *     result += arguments[i] + separator;
 *   }
 *   return result;
 * }
 * 你可以给这个函数传递任意数量的参数，它会将各个参数连接成一个字符串“列表”：
 * console.log(myConcat("、", "红", "橙", "蓝"));
 * // "红、橙、蓝、"
 *
 * console.log(myConcat("；", "大象", "长颈鹿", "狮子", "猎豹"));
 * // "大象；长颈鹿；狮子；猎豹；"
 *
 * console.log(myConcat("。", "智者", "罗勒", "牛至", "胡椒", "香菜"));
 * // "智者。罗勒。牛至。胡椒。香菜。"
 */


//【函数参数】

/**
 * 默认参数
 * 在 JavaScript 中，函数参数的默认值是 undefined
 * 然而在某些情况下设置不同的默认值可能会很有用
 * 这正是默认参数的作用
 *
 * 用于设定默认参数的一般策略是在函数的主体中测试参数值是否为 undefined，如果是则赋予这个参数一个默认值
 *
 * 在下面的示例中，如果调用函数时没有给 b 提供值，那么它的值就是 undefined，在执行 a*b 时，
 * 调用乘法通常会返回 NaN。但是，这已经被示例的第二行所避免了：
 * function multiply(a, b) {
 *   b = typeof b !== "undefined" ? b : 1;
 *   return a * b;
 * }
 *
 * console.log(multiply(5)); // 5
 *
 * 使用默认参数，在函数体的手动检查就不再必要了
 * 现在，你可以在函数头简单地把 1 设定为 b 的默认值：
 * function multiply(a, b = 1) {
 *   return a * b;
 * }
 *
 * console.log(multiply(5)); // 5
 */

/**
 * 剩余参数
 * 剩余参数语法允许将不确定数量的参数表示为数组。
 *
 * 在下面的示例中，multiply 函数使用剩余参数收集从第二个参数开始到最后的参数
 * 然后该函数将它们与第一个参数相乘
 * function multiply(multiplier, ...theArgs) {
 *   return theArgs.map((x) => multiplier * x);
 * }
 *
 * const arr = multiply(2, 1, 2, 3);
 * console.log(arr); // [2, 4, 6]
 */


//【箭头函数】
/**
 * 箭头函数表达式（也称胖箭头，以区分未来 JavaScript 中假设的 -> 语法）
 * 相比函数表达式具有较短的语法且没有它自己的 this、arguments、super 和 new.target
 * 箭头函数总是匿名的
 *
 * 有两个因素会影响对箭头函数的引入：更简洁的函数和 this 的无绑定性
 *
 * 更简洁的函数
 * 示例：
 * const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
 *
 * const a2 = a.map(function (s) {
 *   return s.length;
 * });
 *
 * console.log(a2); // [8, 6, 7, 9]
 *
 * const a3 = a.map((s) => s.length);
 *
 * console.log(a3); // [8, 6, 7, 9]
 */


//【预定义函数】
/**
 * JavaScript 语言有几个顶级的内置函数：
 *
 * eval()
 * eval() 方法执行方法计算以字符串表示的 JavaScript 代码
 *
 * isFinite()
 * isFinite() 全局函数判断传入的值是否是有限的数值。如果需要的话，其参数首先被转换为一个数值
 *
 * isNaN()
 * isNaN() 函数判断一个值是否是 NaN。注意：isNaN 函数内部的强制转换规则十分有趣
 * 你也可以使用 Number.isNaN() 来判断该值是否为 NaN
 *
 * parseFloat()
 * parseFloat() 函数解析字符串参数，并返回一个浮点数
 *
 * parseInt()
 * parseInt() 函数解析字符串参数，并返回指定的基数（基础数学中的数制）的整数
 *
 * decodeURI()
 * decodeURI() 函数对先前经过 encodeURI 函数或者其他类似方法编码过的统一资源标志符（URI）进行解码
 *
 * decodeURIComponent()
 * decodeURIComponent() 方法对先前经过 encodeURIComponent 函数
 * 或者其他类似方法编码的统一资源标志符（URI）进行解码
 *
 * encodeURI()
 * encodeURI() 方法通过以表示字符的 UTF-8 编码的一个、两个、三个或四个转义序列替换统一资源标识符（URI）的某些字符来进行编码
 * （对于由两个“代理（surrogate）”字符组成的字符，只会编码为四个转义序列）
 *
 * encodeURIComponent()
 * encodeURIComponent() 方法通过以表示字符的 UTF-8 编码的一个、两个、三个或四个转义序列替换统一资源标识符（URI）的某些字符来进行编码
 * （对于由两个“代理”字符组成的字符，只会编码为四个转义序列）
 */